# relation_items_controller.pl
use strict;
use warnings;

require '_folders_helper.pl';
require '_items_helper.pl';
require 'relations_helper.pl';

my $FALSE = 0;
my $TRUE  = 1;

my $FOLDER_TYPE    = 'relations';


sub controller_relation_items {
    my ($settings, $params) = @_;

    if (defined $params->{'folder_id'} && $params->{'action'} !~ /^create/) {
        my $is_output = $FALSE;
        my $info = helper_folders_get_info($FOLDER_TYPE, $settings, $params, $is_output);
        if (performed_render()) {
            return;
        }
        if (defined $info && $info->{'mode'} eq 'private') {
            # insist that request is authenticated
            error_unless_authenticated($settings, $params);
        }
    }
}




sub action_relation_items_creating {
    #
    #   Test whether folder method relations_create_from is still in progress
    #
    #   Returns '200 OK' if still in progress; '404 Not Found' if finished.
    #
    my ($settings, $params) = @_;

    my $exists = helper_creating($FOLDER_TYPE, $settings, $params);
    if (performed_render()) {
        return;
    }

    render({
        'status' => ($exists) ? '200 OK' : '404 Not Found',
        'text' => '',
    });
}


sub action_relation_items_count {
    my ($settings, $params) = @_;

    my $is_output = $FALSE;
    my $folder_info = helper_folders_get_info($FOLDER_TYPE, $settings, $params, $is_output);
    if (performed_render()) {
        return;
    }

    my $folder_path = helper_folders_path($FOLDER_TYPE, $settings, $params, $params->{'folder_id'});
    if (performed_render()) {
        return;
    }
    my $number_of_items_in_folder = helper_items_index_size($folder_path);


    render({ 'text' => serialise({'count' => $number_of_items_in_folder}) });
}

sub action_relation_items_index {
    my ($settings, $params) = @_;

    my $page = $params->{'page'};
    my $count = $params->{'count'};

    # fetch a slice of persistent sorted list of items
    my $is_output = $TRUE;
    my $items = helper_items_get_index_range($FOLDER_TYPE, $settings, $params, $page, $count, $is_output);
    if (performed_render()) {
        return;
    }

    render({ 'text' => serialise({'id' => $items}) });
}

sub action_relation_items_list {
    my ($settings, $params) = @_;

    my $page = $params->{'page'};
    my $count = $params->{'count'};

    my $is_output0 = $FALSE;
    my $folder_info = helper_folders_get_info($FOLDER_TYPE, $settings, $params, $is_output0);
    if (performed_render()) {
        return;
    }

    # fetch a slice of persistent sorted list of items
    my $is_output = $TRUE;
    my $items = helper_items_get_range($FOLDER_TYPE, $settings, $params, $page, $count, $is_output);
    if (performed_render()) {
        return;
    }

    render({ 'text' => serialise({'item' => $items}) });
}

sub action_relation_items_create {
    #
    #   Create one or multiple relation items.
    #
    my ($settings, $params) = @_;


    my $exists = helper_folders_exists($FOLDER_TYPE, $settings, $params);
    if (performed_render()) {
        return;
    }
    if (!$exists) {
        #
        # automatically create the target relation
        #
        my $mode = $params->{'mode'};
        my $description = $params->{'description'};
        my $full = $params->{'full'};

        # create relation info hash which will be serialised to a file in the created folder
        my %info = (
              # generic "thing" parameters
            'type'                  => undef,
            'id'                    => undef,
            'mode'                  => $mode,
            'description'           => $description,
            'uri'                   => undef,
        );
        helper_folders_create($FOLDER_TYPE, $settings, $params, \%info);
        if (performed_render()) {
            return;
        }
    }

    my $is_output = $FALSE;
    my $folder_info = helper_folders_get_info($FOLDER_TYPE, $settings, $params, $is_output);
    if (performed_render()) {
        return;
    }

    #my $value = $params->{'value'};
    #   NOTE: we do not copy value param as it may be HUGE. Instead we reference it directly in the code below
    my $from = $params->{'from'};
    my $is_schema = $params->{'is_schema'};
    my $use_schema = $params->{'use_schema'};

    # set id_path to a JSONPath object or undef
    my $id_path;
    if (exists $params->{'id_path'} && $params->{'id_path'} ne '') {
        $id_path = helper_parse_json_path($params->{'id_path'});
        if (performed_render()) {
            return;
        }
    }

    my %items = ();

    if ($from eq 'csv' || $from eq 'arff') {
        #
        # multiple item creation
        #

        my $relation = {
            'attributes'            => [],
                # where each attribute is a hash, { attribute_name, attribute_type, attribute_is_numeric }
            'records'               => [],
        };
        if ($from eq 'csv') {
            my $err;
            my @rows = ();
            try {
#                @rows = @{csv_parse_string( Encode::decode('Detect' ,$params->{'value'}) || '' )};
                @rows = @{csv_parse_string($params->{'value'})};
            } catch {
                my $err = $_;
                if (defined $err && $err ne 'No such file or directory') {      #FIXME: HACK WORKAROUND because 'open'ing from a string gives this error in csv_parse_string
                    warn_message('csv parse error: ' . $err);
                    render({
                        'status' => '400 Bad Request',
                        'text'   => serialise_error_message('Invalid value'),
                    });
                    return;
                }
            };
            if ($is_schema && scalar(@rows) > 0) {
                # consume first row of csv as column headings row
                my $names = shift @rows;
                foreach my $name (@$names) {
                    push(@{$relation->{'attributes'}}, {
                        'attribute_name' => $name,
                        'attribute_type' => 'STRING',
                        'attribute_is_numeric' => $FALSE,
                    });
                }
            }
            $relation->{'records'} = \@rows;
        }
        elsif ($from eq 'arff') {
            require 'arff.pl';
    #            $relation = arff_parse_string($params->{'value'}, (($is_schema && $use_schema) ? $TRUE : $FALSE));
            $relation = arff_parse_string($params->{'value'}, ($use_schema ? $TRUE : $FALSE));
        }

        # create an item for each row

        my $i = 0;
        if (!defined $id_path) {
            # generate ids with numbers starting from current no. items in folder (assumes ids are not already in use)
            my $folder_path = helper_folders_path($FOLDER_TYPE, $settings, $params, $params->{'folder_id'});
            if (performed_render()) {
                return;
            }
            $i = helper_items_index_size($folder_path);
        }
        foreach my $item_value (@{$relation->{'records'}}) {
            # generate an automatic item_id
            $i++;
            my $item_id = sprintf('i%06u', $i); #left pad with zeros so alpha sort order is same as numeric sequence
            if (defined $id_path) {
                # fetch field from specified JSON Path to use as an item_id
                my $v_arrayref = helper_apply_json_path($id_path, $item_value);
                if (performed_render()) {
                    return;
                }
                if (scalar(@$v_arrayref) >= 1) {
                    $item_id = util_getValidFileName( join('_', @$v_arrayref) );
                }
            }

            if ($from eq 'csv' && $is_schema && $use_schema) {  #if arff than atttributes already created
                my %value = ();
                my $attributes = $relation->{'attributes'};
                my $attribute_count = scalar(@$attributes);
                for(my $i=0; $i < $attribute_count; $i++) {
                    $value{$attributes->[$i]{'attribute_name'}} = $item_value->[$i];
                }
                $item_value = \%value;
            }

            $items{$item_id} = $item_value;
        }
    }
    else {
        #
        # single item creation
        #

        # force to valid id (NB: we abuse fact that our valid filenames are also valid ids)
        my $item_id = util_getValidFileName( $params->{'item_id'} );
        $params->{'item_id'}     = $item_id;

#        my $item_value_str  = $params->{'value'} || '';
        my $item_value_str  = Encode::decode('Detect' ,$params->{'value'}) || '';

        # normalise newlines in text value
        $item_value_str =~ s/\r\n|\r/\n/gmxs;

        if ($item_value_str eq '') {
            render({
                'status' => '400 Bad Request',
                'text'   => serialise_error_message('Missing parameter: value'),
            });
            return;
        }

        my $item_value;
        if ($from eq 'json') {
            try {
                $item_value = JSON->new->allow_nonref->decode($item_value_str);
            } catch {
                # trim off perl code details from end of error message
                $_ =~ s/\)\sat\s.*//xms;
                render({
                    'status' => '400 Bad Request',
                    'text'   => serialise_error_message('Invalid json value - ' .  $item_value_str . ', ' . $_),
                });
                return;
            };
        }
        elsif ($from eq 'xml') {
            require XML::Simple;
            # strip off xml directives as they easily confuse XML::Simple
            $item_value_str =~ s/<\?(?:.*?)\?>//gxsm;
            try {
                $item_value = XML::Simple::XMLin($item_value_str, 
                    'ContentKey' => 'text', 'ForceArray' => 0, 'KeepRoot' => 1
                );
            } catch {
                # trim off perl code details from end of error message
                $_ =~ s/\)\sat\s.*//xms;
                render({
                    'status' => '400 Bad Request',
                    'text'   => serialise_error_message('Invalid xml value - ' .  $item_value_str . ', ' .$_),
                });
                return;
            };
        }
        else {
            # assume $from eq 'text', so encode as scalar string (extension to JSON standard but supported by browsers)
            $item_value = Encode::decode('Detect', $item_value_str);
        }

        $items{$item_id} = $item_value;
    }

    my @item_ids = keys %items;

    #FIXME: make replace a cgi parameter so user can specify behaviour
    my $replace = $FALSE;
    if (!$replace) {
        # check for prior existence of item of any of the items
        if (helper_items_exists($FOLDER_TYPE, $settings, $params, @item_ids)) {
            if (performed_render()) {
                return;
            }
            render({
                'status' => '403 Forbidden',
                'text'   => serialise_error_message("Relation item already exists"),
            });
            return;
        }
    }

    # create one or more persistent items
    helper_items_create($FOLDER_TYPE, $settings, $params, \%items);
    if (performed_render()) {
        return;
    }

#    my $text = serialise(
#        ((scalar(@items_info) > 1) ? {'item' => \@items_info} : {'item' => $items_info[0]})
#    );
    my $text = serialise( {'item_count' => scalar(@item_ids)} );

    # construct uri for the resources being created
    my $user_id = $params->{'user_id'};
    my $folder_id = $params->{'folder_id'};
    my $uri_stem = $settings->{'SITE_URL'} . '/' . $user_id . '/' . $FOLDER_TYPE . '/' . $folder_id . '/items';
    render({
        'status'    => '201 Created',
        'headers'   => { 'Location' => $uri_stem },
        'text'      => $text,
    });
}


sub action_relation_items_destroy {
    my ($settings, $params) = @_;

    my $is_output0 = $FALSE;
    my $folder_info = helper_folders_get_info($FOLDER_TYPE, $settings, $params, $is_output0);
    if (performed_render()) {
        return;
    }
    
    my $item_id = $params->{'item_id'};

    my $is_output = $TRUE;
    my $deleted_item = helper_items_get($FOLDER_TYPE, $settings, $params, $item_id, $is_output);
    if (performed_render()) {
        return;
    }
    if (!defined $deleted_item) {
        render({
            'status' => '404 Not Found',
            'text'   => serialise_error_message("No such item: $item_id"),
        });
        return;
    }
    
    # delete item from items database
    helper_items_destroy($FOLDER_TYPE, $settings, $params, $item_id);
    if (performed_render()) {
        return;
    }

#    my $text = serialise({'item' => $deleted_item});
#    render({ 'text' => $text });
    render({ 'text' => 'ok' });
}


sub action_relation_items_destroy_all {
    my ($settings, $params) = @_;

    my $is_output = $FALSE;
    my $folder_info = helper_folders_get_info($FOLDER_TYPE, $settings, $params, $is_output);
    if (performed_render()) {
        return;
    }

    # delete all item from items database
    helper_items_destroy_all($FOLDER_TYPE, $settings, $params);
    if (performed_render()) {
        return;
    }

    render({ 'text' => 'ok' });
}


sub action_relation_items_exists {
    my ($settings, $params) = @_;

    my $exists = helper_folders_exists($FOLDER_TYPE, $settings, $params);
    if (performed_render()) {
        return;
    }

    my $item_id = $params->{'item_id'};

    if ($exists) {
        my $is_output = $FALSE;
        my $folder_info = helper_folders_get_info($FOLDER_TYPE, $settings, $params, $is_output);
        if (performed_render()) {
            return;
        }

        $exists = helper_items_exists($FOLDER_TYPE, $settings, $params, $item_id);
        if (performed_render()) {
            return;
        }
    }

    render({
        'text' => '',
        'status' => ($exists) ? '200 OK' : '404 Not Found',
    });
}


sub action_relation_items_show {
    my ($settings, $params) = @_;

    my $is_output = $TRUE;
    my $folder_info = helper_folders_get_info($FOLDER_TYPE, $settings, $params, $is_output);
    if (performed_render()) {
        return;
    }

    my $item_id = $params->{'item_id'};

#    my $is_output = $TRUE;
    my $item = helper_items_get($FOLDER_TYPE, $settings, $params, $item_id, $is_output);
    if (performed_render()) {
        return;
    }
    if (!defined $item) {
        render({
            'status' => '404 Not Found',
            'text'   => serialise_error_message("No such item: $item_id"),
        });
        return;
    }

    render({ 'text' => serialise({'item' => {$item_id => $item}}) });
}


sub action_relation_items_update {
    my ($settings, $params) = @_;

    my $item_id = $params->{'item_id'};
    my $item_value_str = Encode::decode('Detect', $params->{'value'}) || '';

    my $is_output0 = $FALSE;
    my $folder_info = helper_folders_get_info($FOLDER_TYPE, $settings, $params, $is_output0);
    if (performed_render()) {
        return;
    }

    my $is_output = $FALSE; ###FIXME: actually, we return the current value (could just return _show instead)
    my $old_item_value = helper_items_get($FOLDER_TYPE, $settings, $params, $item_id, $is_output);
    if (performed_render()) {
        return;
    }
    if (!defined $old_item_value) {
        render({
            'status' => '404 Not Found',
            'text'   => serialise_error_message("No such item: $item_id"),
        });
        return;
    }

    my $changed = $FALSE;
    my $item_value;
    if ($item_value_str ne '') {
        try {
            $item_value = JSON->new->allow_nonref->decode($item_value_str);
        } catch {
            # trim off perl code details from end of error message
            $_ =~ s/\)\sat\s.*//xms;
            render({
                'status' => '400 Bad Request',
                'text'   => serialise_error_message('Invalid value - ' . $_),
            });
            return;
        };
        #FIXME: should deep compare with old_item_value rather than assuming it has changed (could do by canonical string compare)
        $changed = $TRUE;
    }
    if ($changed) {
        helper_items_put($FOLDER_TYPE, $settings, $params, $item_id, $item_value);
        if (performed_render()) {
            return;
        }
    }

    render({ 'text' => serialise({'item' => {$item_id => $item_value}}) });
}


1;